package ua.edu.nuos.jeetraining2012.cms.service;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.subject.Subject;
import org.scribe.up.profile.CommonProfile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ua.edu.nuos.jeetraining2012.cms.dto.predefined.StandardUserStatuses;
import ua.edu.nuos.jeetraining2012.cms.entity.StudentEntity;
import ua.edu.nuos.jeetraining2012.cms.entity.UserEntity;
import ua.edu.nuos.jeetraining2012.cms.model.UserDAO;
import ua.edu.nuos.jeetraining2012.cms.model.exception.DAOException;
import ua.edu.nuos.jeetraining2012.cms.service.exception.ServiceException;
import ua.edu.nuos.jeetraining2012.cms.service.exception.ServiceValidationException;
import ua.edu.nuos.jeetraining2012.cms.util.LocaleWrapper;
import ua.edu.nuos.jeetraining2012.cms.util.interceptor.ShiroSecurity;

import javax.annotation.Resource;
import javax.ejb.EJB;
import javax.ejb.Local;
import javax.ejb.Stateless;
import javax.validation.ConstraintViolation;
import javax.validation.ValidatorFactory;
import java.security.SecureRandom;
import java.util.*;


/**
 * Class for managing User entity
 */
@Stateless
@Local
@ShiroSecurity
public class User {

    /**
     * Define logger
     */
    final private static Logger logger = LoggerFactory.getLogger(User.class);
    @SuppressWarnings({"EjbEnvironmentInspection"})
    @Resource
    private ValidatorFactory validatorFactory;

    @SuppressWarnings({"EjbEnvironmentInspection"})
    @EJB
    private UserDAO dao;

    @SuppressWarnings({"EjbEnvironmentInspection"})
    @EJB
    private Student student;


    /**
     * Default constructor
     */
    public User() {
    }


    /**
     * Constructor with custom validation and dao
     */
    public User(ValidatorFactory validationFactory, UserDAO dao) {
        this.validatorFactory = validationFactory;
        this.dao = dao;
    }

    /**
     * Check if user is registered in storage by using user id from DTO.
     * Return true if user is registered, false otherwise
     *
     * @param user UserEntity id to be searched for
     * @return True if user is registered, false otherwise
     * @throws ua.edu.nuos.jeetraining2012.cms.service.exception.ServiceValidationException
     *                                  When DTO id is invalid
     * @throws IllegalArgumentException When UserEntity is null
     * @throws ServiceException         When underlying storage throw an exception
     */
    public boolean isExist(UserEntity user) throws ServiceValidationException, ServiceException {
        return (findById(user) != null);
    }

    /**
     * Persist user in storage. If this is new user, new entry will be added otherwise existed user is updated
     *
     * @param user UserEntity to be persisted
     * @throws ua.edu.nuos.jeetraining2012.cms.service.exception.ServiceValidationException
     *                                  When UserEntity values are invalid
     * @throws IllegalArgumentException When UserEntity is null
     * @throws ServiceException         When underlying storage throw an exception
     */
    public void persist(UserEntity user) throws ServiceValidationException, ServiceException {
        if (user == null)
            throw new IllegalArgumentException("DTO can't be null");

        boolean exist = isExist(user);
        Set<ConstraintViolation<UserEntity>> validationResult = validatorFactory.getValidator().validate(user);
        if (!validationResult.isEmpty()) {
            throw new ServiceValidationException(validationResult);
        }
        try {
            if (exist) {
                dao.update(user);
            } else {
                dao.create(user);
            }
        } catch (DAOException e) {
            logger.error("User persist received DAO Exception", e);
            throw new ServiceException("User persist received DAO Exception: " + e.getMessage());
        }
    }

    /**
     * Deletes user from the storage by using user id from DTO.
     * Return true if user is deleted,false otherwise.
     *
     * @param user DTO with user id
     * @return true if deleted, false otherwise
     * @throws ua.edu.nuos.jeetraining2012.cms.service.exception.ServiceValidationException
     *                          When UserEntity id is invalid
     * @throws ServiceException When underlying storage throw an exception
     */
    public boolean delete(UserEntity user) throws ServiceValidationException, ServiceException {
        if (user == null)
            throw new IllegalArgumentException("DTO can't be null");

        if (!isExist(user))
            return false;

        try {
            return dao.delete(user);
        } catch (DAOException e) {
            logger.error("User delete received DAO Exception", e);
            throw new ServiceException("User delete received DAO Exception: " + e.getMessage());
        }
    }

    /**
     * Update user in storage.
     * @param user DTO with user id
     * @throws ServiceException When underlying storage throw an exception
     * @throws ServiceValidationException When UserEntity id is invalid
     */
    public void update(UserEntity user) throws ServiceException, ServiceValidationException {
        if (user == null) {
            throw new IllegalArgumentException("DTO can't be null");
        }

        try {
            if (isExist(user)) {
                dao.update(user);
            }
        } catch (DAOException e) {
            throw new ServiceException("User update received DAO Exception: " + e.getMessage());
        }
    }

    /**
     * Method create a full user profile, that consist of users and students table data.
     * @param user DTO with user id
     * @throws ServiceException if can't get all data about user profile
     * @return Map with user profile data
     */
    public Map<String, Object> getUserProfile(UserEntity user) throws ServiceException {
        logger.debug("getUserProfile() - invocation");

        if (user == null) {
            throw new IllegalArgumentException("DTO can't be null");
        }

        Map<String, Object> profileData;
        try {
            if (isExist (user)) {

                StudentEntity studentEntity = new StudentEntity();
                studentEntity.setUserId(user);
                profileData = student.getStudentProfile(studentEntity);

                user = findById(user);
                profileData.put("vkId", user.getId());
                profileData.put("name", user.getName());
                profileData.put("mail", user.getEmail());
            }else {
                logger.warn("ServiceException - User not exist");
                throw new ServiceException("User not exist");
            }
        } catch (ServiceValidationException e) {
            logger.warn("ServiceValidationException - " + e.getMessage());
            throw  new ServiceException("ServiceValidationException - "+e.getMessage());
        } catch (ServiceException e) {
            logger.warn("ServiceException - " + e.getMessage());
            throw  new ServiceException("ServiceException - "+e.getMessage());
        }

        return profileData;
    }

    /**
     * Loads UserEntity with values of current user supplied by Shiro library and return it.
     *
     * @return DTO with values of current user
     * @throws ServiceException When no CommonProfile object was provided by Shiro
     */
    @RequiresAuthentication
    public UserEntity loadDTOShiro() throws ServiceException {
        CommonProfile profile = null;
        Subject subject = SecurityUtils.getSubject();
        UserEntity dto = new UserEntity();

        dto.setId(subject.getPrincipals().getPrimaryPrincipal().toString());
        for (Object o : subject.getPrincipals().asList()) {
            logger.debug("Principal classname {} value {}", o.getClass(), o);
            if (o instanceof CommonProfile) {
                profile = (CommonProfile) o;
                break;
            }
        }
        if (profile == null) {
            logger.warn("loadDTOShiro() No CommonProfile object was found");
            throw new ServiceException("No CommonProfile object was found");
        }
        String firstName = (profile.getFirstName() == null) ? "" : profile.getFirstName();
        String lastName  = (profile.getFamilyName() == null) ? "" : profile.getFamilyName();
        dto.setEmail(profile.getEmail());
        if (firstName.isEmpty() && lastName.isEmpty()) {
            try {
                ResourceBundle i18n = getResourceBundle();
                dto.setName(i18n.getString("anonymous"));
            } catch (MissingResourceException | NullPointerException e) {
                dto.setName("Аноним");
            }
        } else if (firstName.isEmpty() || lastName.isEmpty()) {
            dto.setName(firstName.isEmpty() ? lastName : firstName);
        } else
            dto.setName(String.format("%s %s", firstName, lastName));
        return dto;
    }

    /**
     * Return ResourceBundle which contains i18n used in User Class
     *
     * @return resource bundle
     * @throws MissingResourceException if resource file not found
     */
    public ResourceBundle getResourceBundle() {
        return LocaleWrapper.getBundle("general");
    }

    /**
     * Set necessary dto fields to predefined/default values for user being registered.
     *
     * @param dto DTO values to be set
     * @throws NullPointerException if dto is null
     */
    public void setRegistrationValues(UserEntity dto) {
        dto.setRole("student");
        dto.setStatus(StandardUserStatuses.APPROVING);
    }

    /**
     * Lookup user in a storage and return DTO. Only id field is used during lookup
     *
     * @param user UserEntity id to be searched for
     * @return dto with filled user values, null if not found
     * @throws ServiceValidationException When DTO id is invalid
     * @throws IllegalArgumentException   When dto is null
     * @throws ServiceException           When underlying storage throw an exception
     */
    public UserEntity findById(UserEntity user) throws ServiceException, ServiceValidationException {
        if (user == null)
            throw new IllegalArgumentException("DTO can't be null");
        Set<ConstraintViolation<UserEntity>> validationResult = validatorFactory.getValidator().validateValue(UserEntity.class, "id", user.getId());
        if (!validationResult.isEmpty()) {
            throw new ServiceValidationException(validationResult);
        }
        try {
            return dao.find(user);
        } catch (DAOException e) {
            logger.error("Received DAO Exception", e);
            throw new ServiceException("Received DAO Exception: " + e.getMessage());
        }
    }

    /**
     * Lookup user in a storage by his email and return DTO.
     *
     * @param email User email to be searched for
     * @return dto with filled user values, null if not found
     * @throws ServiceException           When underlying storage throw an exception
     * @throws IllegalArgumentException   When email is null
     * @throws ServiceValidationException When email is invalid
     */
    public UserEntity findByEmail(String email) throws ServiceException, ServiceValidationException {
        if (email == null)
            throw new IllegalArgumentException("DTO can't be null");
        Set<ConstraintViolation<UserEntity>> validationResult = validatorFactory.getValidator().validateValue(UserEntity.class, "email", email);
        if (!validationResult.isEmpty()) {
            throw new ServiceValidationException(validationResult);
        }
        try {
            return dao.findByEmail(email);
        } catch (DAOException e) {
            logger.error("Received DAO Exception", e);
            throw new ServiceException("Received DAO Exception: " + e.getMessage());
        }
    }

    /**
     * Return user md5 hash.
     *
     * @param email Email of the user hash to be computed
     * @return Computed md5 hash
     */
    public String getMd5(String email) throws ServiceException, ServiceValidationException {
        Set<ConstraintViolation<UserEntity>> validationResult = validatorFactory.getValidator().validateValue(UserEntity.class, "email", email);
        if (!validationResult.isEmpty()) {
            throw new ServiceValidationException(validationResult);
        }
        try {
            return dao.approveChecksum(email);
        } catch (DAOException e) {
            logger.error("getMd5() - DAO Exception ", e);
            throw new ServiceException("getMd5() - DAO Exception " + e.getMessage());
        }
    }

    /**
     * Compute user hash and validate against hash value from storage.
     * If hash are matched user status changed to activated.
     *
     * @param email   User to be searched by its email
     * @param userMd5 User supplied md5 hash value
     * @return User friendly activation result message
     * @throws IllegalArgumentException   When email or md5 are null
     * @throws ServiceException           When underlying storage throw an exception
     * @throws ServiceValidationException When email is invalid
     */
    public String activate(String email, String userMd5) throws ServiceException, ServiceValidationException {
        if ((email == null) || (userMd5 == null))
            throw new IllegalArgumentException("Email and hash value must not be null");

        Set<ConstraintViolation<UserEntity>> validationResult = validatorFactory.getValidator().validateValue(UserEntity.class, "email", email);
        if (!validationResult.isEmpty()) {
            throw new ServiceValidationException(validationResult);
        }
        try {
            ResourceBundle bundle = getResourceBundle();
            UserEntity dto = dao.findByEmail(email);
            String computedMd5 = getMd5(email);
            if ((dto == null) || (computedMd5 == null)) {
                return bundle.getString("activateUserNotFound");
            }
            if (!computedMd5.equals(userMd5)) {
                return bundle.getString("activateUserWrongURL");
            }
            dto.setStatus(StandardUserStatuses.ACTIVE);
            persist(dto);
            logger.debug("activate() User {} activated ", email);
            return bundle.getString("activateSuccessful");
        } catch (DAOException e) {
            logger.error("getMd5() - DAO Exception ", e);
            throw new ServiceException("getMd5() - DAO Exception " + e.getMessage());
        }
    }

    /**
     * Is user email confirmed?
     * <p/>
     * Return <code>true</code> if user confirmed his / her Email address by clicking on confirmation link in sent Email.
     * In other cases: got DAOException, can't find user or user not confirmed email yet, <code>false</code> will
     * be returned.
     * <p/>
     * If user ID is <code>null</code> or empty, IllegalArgumentException will be thrown.
     *
     * @param userId                        User ID
     * @return                              True if user confirmed Email, either -- false.
     * @throws IllegalArgumentException     If userId is null or empty.
     */
    public boolean isEmailConfirmed(String userId) throws IllegalArgumentException {
        logger.debug("isEmailConfirmed() - started.");

        if (userId == null || userId.isEmpty()) {
            logger.error("User ID can't be {}", userId);
            logger.debug("isEmailConfirmed() - finished with IllegalArgumentException.");
            throw new IllegalArgumentException("User ID can't be null or empty.");
        }

        boolean isConfirmed = false;
        try {
            UserEntity entity = dao.findById(userId);
            if (entity != null) {
                isConfirmed = (entity.getStatus().equals(StandardUserStatuses.ACTIVE));
            }
        } catch (DAOException e) {
            logger.error("Got DAOException with message {} and \ntrace:\n {}", e.getMessage(),
                    e.getStackTrace());
            logger.debug("Return result is false.");
        }

        logger.info("Return result is {}", isConfirmed);

        logger.debug("isEmailConfirmed() - finished.");
        return isConfirmed;
    }


    /**
     * Method return list of found entities instance or null if the entities does not exist
     * @return List of found entities instance or null if the entities does not exist
     * @throws ServiceException
     */
    public List<UserEntity> getAvailableUsers() throws ServiceException {
        logger.debug("Method getAvailableUsers () - invocation.");
        List<UserEntity> availableUsers;

        try {
            availableUsers = new ArrayList<>();
            for (UserEntity currentEntity : dao.findAll()) {
                availableUsers.add(currentEntity);
            }
        } catch (DAOException e) {
            logger.debug("Method getAvailableCourses () - finish with DAOException.");
            throw new ServiceException("getAvailableCourses Exception" + e.getMessage());
        }
        return availableUsers;
    }

    /**
     * Generate User's SVN Password
     *
     * @return String generated password
     */
    public String createUserPassword() {
        Random random = new SecureRandom();
        String letters = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ123456789";

        String password = "";
        for (int i = 0; i < 8; i++) {
            int index = (int) (random.nextDouble() * letters.length());
            password += letters.substring(index, index + 1);
        }
        return password;
    }
}
